﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using jabber.client;
using System.Collections;

namespace RoketPack.IM
{
    class Session
    {
        private JabberClient m_Client = null;
        private RosterManager m_Roster = null;
        private PresenceManager m_Presence = null;
        private string p_Username = null;
        private string p_Server = null;
        private string p_Password = null;
        private List<Contact> p_Contacts = new List<Contact>();
        private MessageQueue p_Queue = null;
        private string p_Status = null;
        private Contact.StateEnum p_State = Contact.StateEnum.OFFLINE;
        private NotificationManager m_Notifications = null;
        private bool m_Connected = false;

        /// <summary>
        /// This event is raised when a contact's status has been updated.
        /// </summary>
        internal event ContactEventHandler StatusUpdated;

        /// <summary>
        /// This event is raised when a contact has been added.
        /// </summary>
        internal event ContactEventHandler ContactAdded;

        /// <summary>
        /// This event is raised when a contact has been changed.
        /// </summary>
        internal event ContactEventHandler ContactChanged;

        /// <summary>
        /// This event is raised when a contact is removed.
        /// </summary>
        internal event ContactEventHandler ContactRemoved;

        /// <summary>
        /// This event is raised when a connection starts
        /// </summary>
        internal event EventHandler ConnectStart;

        /// <summary>
        /// This event is raised when a connection succeeds
        /// </summary>
        internal event EventHandler ConnectSuccess;

        /// <summary>
        /// This event is raised when a connection fails
        /// </summary>
        internal event EventHandler ConnectFailure;

        #region Event Helpers
        protected virtual void OnStatusUpdated(Contact contact)
        {
            if (this.StatusUpdated != null)
                this.StatusUpdated(this, new ContactEventArgs(contact, ContactEventArgs.ActionType.STATUS));
        }
        protected virtual void OnContactAdded(Contact contact)
        {
            if (this.ContactAdded != null)
                this.ContactAdded(this, new ContactEventArgs(contact, ContactEventArgs.ActionType.ADDED));
        }
        protected virtual void OnContactChanged(Contact contact)
        {
            if (this.ContactChanged != null)
                this.ContactChanged(this, new ContactEventArgs(contact, ContactEventArgs.ActionType.CHANGED));
        }
        protected virtual void OnContactRemoved(Contact contact)
        {
            if (this.ContactRemoved != null)
                this.ContactRemoved(this, new ContactEventArgs(contact, ContactEventArgs.ActionType.REMOVED));
        }
        protected virtual void OnConnectStart()
        {
            if (this.ConnectStart != null)
                this.ConnectStart(this, new EventArgs());
        }
        protected virtual void OnConnectSuccess()
        {
            if (this.ConnectSuccess != null)
                this.ConnectSuccess(this, new EventArgs());
        }
        protected virtual void OnConnectFailure()
        {
            if (this.ConnectFailure != null)
                this.ConnectFailure(this, new EventArgs());
        }
        #endregion

        /// <summary>
        /// Creates a new XMPP session with the specified username,
        /// server and password settings.  Use Connect() to connect
        /// to the XMPP server.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="domain">The server.</param>
        /// <param name="password">The password.</param>
        public Session(string server, string username, string password)
        {
            // Set the basic variables.
            this.p_Server = server;
            this.p_Username = username;
            this.p_Password = password;
        }

        /// <summary>
        /// Connects to the server.
        /// </summary>
        public void Connect()
        {
            if (this.m_Connected)
                throw new InvalidOperationException("Can not connect to server when connecting or already connected.");

            // Create a new client.
            this.m_Client = new JabberClient();
            if (this.p_Queue != null) this.p_Queue.Close();
            this.p_Queue = new MessageQueue(this.m_Client);
            this.m_Client.Server = this.p_Server;
            this.m_Client.User = this.p_Username;
            this.m_Client.Password = this.p_Password;
            this.m_Client.AutoRoster = true;
            this.m_Client.AutoPresence = false;
            this.m_Client.OnAuthenticate += new bedrock.ObjectHandler(m_Client_OnAuthenticate);
            this.m_Client.OnAuthError += new jabber.protocol.ProtocolHandler(m_Client_OnAuthError);
            this.m_Client.OnBeforePresenceOut += new PresenceHandler(m_Client_OnBeforePresenceOut);
            this.m_Client.OnConnect += new jabber.connection.StanzaStreamHandler(m_Client_OnConnect);
            this.m_Client.OnDisconnect += new bedrock.ObjectHandler(m_Client_OnDisconnect);
            this.m_Client.OnProtocol += new jabber.protocol.ProtocolHandler(m_Client_OnProtocol);
            this.m_Client.OnError += new bedrock.ExceptionHandler(m_Client_OnError);

            // Create and attach a roster manager to the client.
            this.m_Roster = new RosterManager();
            this.m_Roster.Stream = this.m_Client;
            this.m_Roster.AutoSubscribe = true;
            this.m_Roster.AutoAllow = AutoSubscriptionHanding.AllowIfSubscribed;
            this.m_Roster.OnRosterBegin += new bedrock.ObjectHandler(m_Roster_OnRosterBegin);
            this.m_Roster.OnRosterItem += new RosterItemHandler(m_Roster_OnRosterItem);
            this.m_Roster.OnRosterEnd += new bedrock.ObjectHandler(m_Roster_OnRosterEnd);
            this.m_Roster.OnSubscription += new SubscriptionHandler(m_Roster_OnSubscription);

            // Create and attach a new presence manager to the client.
            this.m_Presence = new PresenceManager();
            this.m_Presence.Stream = this.m_Client;
            this.m_Presence.OnPrimarySessionChange += new PrimarySessionHandler(m_Presence_OnPrimarySessionChange);

            // Create the notifications manager.
            if (this.m_Notifications != null) this.m_Notifications.Close();
            this.m_Notifications = new NotificationManager(System.Windows.Forms.Screen.PrimaryScreen);

            // Now attempt the connection.
            this.m_Client.Connect();
            this.OnConnectStart();
        }

        /// <summary>
        /// Disconnects the current session.
        /// </summary>
        public void Disconnect()
        {
            // Set the basic variables.
            this.m_Client.Close();
            this.p_Queue.Close();
            this.m_Notifications.Close();
            this.m_Client = null;
        }

        /// <summary>
        /// Clears all of the currently shown notifications.
        /// </summary>
        internal void ClearNotifications()
        {
            this.m_Notifications.Clear();
        }

        /// <summary>
        /// This event is raised when the roster starts loading all of the
        /// user's contacts.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        void m_Roster_OnRosterBegin(object sender)
        {
        }

        /// <summary>
        /// This event is raised when the roster has loaded a specific contact
        /// and we now need to handle it (by adding them to the list of contacts
        /// or whatever).
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="ri">The contact.</param>
        void m_Roster_OnRosterItem(object sender, jabber.protocol.iq.Item ri)
        {
            // Check to see if it already exists in the contacts list.
            Contact existing = null;
            foreach (Contact c in this.p_Contacts)
                if (c.ID == ri.JID.Bare)
                {
                    existing = c;
                    break;
                }

            if (existing == null)
            {
                // Contact added.
                Contact c = new Contact(ri, this.m_Presence[ri.JID]);
                this.p_Contacts.Add(c);
                this.OnContactAdded(c);
            }
            else
            {
                this.OnContactChanged(existing);
            }
        }

        /// <summary>
        /// This event is raised when the roster has finished loading all of
        /// the user's contacts.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        void m_Roster_OnRosterEnd(object sender)
        {
        }

        /// <summary>
        /// This event is fired when a new contact has requested to subscribe to the
        /// current user.  Offer the user the ability to accept or deny the request.
        /// </summary>
        /// <param name="manager">The roster manager.</param>
        /// <param name="ri">The contact that wants to be added.</param>
        /// <param name="pres">The presence information for the contact.</param>
        void m_Roster_OnSubscription(RosterManager manager, jabber.protocol.iq.Item ri, jabber.protocol.client.Presence pres)
        {
            Contact c = new Contact(pres.From, this.m_Presence);
            c.State = Contact.StateEnum.ONLINE;
            SubscriptionForm f = new SubscriptionForm(c);
            if (f.ShowDialog() == System.Windows.Forms.DialogResult.Yes)
                manager.ReplyAllow(pres);
            else
                manager.ReplyDeny(pres);
        }

        /// <summary>
        /// This event is raised when one of the contact's status changes.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="bare">The contact whose status has changed.</param>
        void m_Presence_OnPrimarySessionChange(object sender, jabber.JID bare)
        {
            foreach (Contact c in this.p_Contacts)
            {
                if (c.ID == bare.Bare)
                {
                    if (this.m_Presence[bare] == null)
                        continue;

                    bool wasoffline = !c.Online;
                    c.Status = this.m_Presence[bare].Status;
                    c.State = Contact.GetStateFromPresence(this.m_Presence[bare]);
                    c.OnStatusUpdated();
                    this.OnStatusUpdated(c);

                    if (wasoffline && c.Online)
                        this.m_Notifications.Add(c.Representation + " is now online");
                    return;
                }
            }

            // Check for current user.
            if (bare.User == this.p_Username && bare.Server == this.p_Server && this.m_Presence[bare] != null)
            {
                this.p_Status = this.m_Presence[bare].Status;
                this.p_State = Contact.GetStateFromPresence(this.m_Presence[bare]);
                this.OnStatusUpdated(new Contact(bare, this.m_Presence));
            }
        }

        /// <summary>
        /// This event is raised when the user has been successfully authenticated.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        void m_Client_OnAuthenticate(object sender)
        {
            this.m_Connected = true;
            this.OnConnectSuccess();

            this.p_Status = "";
            this.SetUserState(Contact.StateEnum.ONLINE);
        }

        /// <summary>
        /// This event is raised when there was an error authenticating
        /// the user with the remote server.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="rp"></param>
        void m_Client_OnAuthError(object sender, System.Xml.XmlElement rp)
        {
            this.OnConnectFailure();
        }

        /// <summary>
        /// This event is raised when the user's status or state has changed
        /// and must be updated on the UI.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="pres"></param>
        void m_Client_OnBeforePresenceOut(object sender, jabber.protocol.client.Presence pres)
        {
            this.p_Status = pres.Status;
            this.p_State = Contact.GetStateFromPresence(pres);
            this.OnStatusUpdated(new Contact(new jabber.JID(this.p_Username, this.p_Server, "RoketPack"), null));
        }

        /// <summary>
        /// This event is raised when a general error occurs.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="ex">The exception that was raised.</param>
        void m_Client_OnError(object sender, Exception ex)
        {
            this.OnConnectFailure();
        }

        /// <summary>
        /// This event is raised when a new, general element arrives
        /// through the protocol.  This event handles both standard
        /// text messages and RoketPack notification elements.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="rp">The general XML element</param>
        void m_Client_OnProtocol(object sender, System.Xml.XmlElement rp)
        {
            // Attempt to decode the element as a standard text message.
            jabber.protocol.client.Message msg = rp as jabber.protocol.client.Message;
            if (msg != null)
            {
                switch (rp.FirstChild.Name)
                {
                    case "rp":
                        // It's a specialized RoketPack notification.
                        CustomPacket p = CustomPacket.GetCustomPacketFromNode(rp.GetElementsByTagName("rp")[0]);
                        switch (p.Type)
                        {
                            case "notification":
                                this.m_Notifications.Add(p.Message);
                                break;
                        }

                        break;
                    default:
                        // It's a standard text node.
                        if (msg.Body != null)
                        {
                            Contact contact = null;
                            foreach (Contact c in this.p_Contacts)
                            {
                                if (c.ID == msg.From.Bare)
                                {
                                    contact = c;
                                    break;
                                }
                            }
                            if (contact == null)
                                contact = new Contact(msg.From, this.m_Presence);

                            this.p_Queue.Recieve(contact, msg.Body);
                        }

                        break;
                }
            }
        }

        /// <summary>
        /// This event is raised when the client has connected to
        /// the server.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="stream">The connection information.</param>
        void m_Client_OnConnect(object sender, jabber.connection.StanzaStream stream)
        {
            // We don't set m_Connected to true here as we
            // wait until we finish authenticating.
        }

        /// <summary>
        /// This event is raised when the client has disconnected
        /// from the server.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        void m_Client_OnDisconnect(object sender)
        {
            this.Disconnect();
            this.m_Connected = false;
        }

        /// <summary>
        /// This function sends a message to the specified contact with
        /// the specified message.
        /// </summary>
        /// <param name="contact">The contact to send the message to.</param>
        /// <param name="message">The message to send.</param>
        internal void SendMessage(Contact contact, string message)
        {
            this.m_Client.Message(contact.User + "&" + contact.Server, message);
        }

        /// <summary>
        /// Announces that this user has started an application in RoketPack,
        /// sending the message to all their currently online contacts (only
        /// RoketPack clients will pick up the notification element, so it won't
        /// interfere with text conversations).
        /// </summary>
        internal void AnnounceApplicationExecution(string name)
        {
            foreach (Contact c in this.p_Contacts)
            {
                if (c.Online)
                {
                    this.p_Queue.SendNotification(c, this.UserRepresentation + " is now playing " + name + ".");
                }
            }
        }

        /// <summary>
        /// Adds a new contact to the user's roster via the Subscribe function.
        /// </summary>
        /// <param name="contact">The contact's username.</param>
        /// <param name="nickname">The contact's nickname.</param>
        internal void AddContact(string contact, string nickname)
        {
            if (!contact.Contains("@"))
                System.Windows.Forms.MessageBox.Show("The contact's username did not contain a domain component, seperated by the @ symbol.", "Invalid Contact Username", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
            string[] s = contact.Split(new char[] { '@' });

            if (nickname.Trim() == "") nickname = contact;
            this.m_Client.Subscribe(new jabber.JID(s[0], s[1], ""), nickname, new string[] { "Friends" });
        }

        /// <summary>
        /// Renames a contact on the user's roster.
        /// </summary>
        /// <param name="contact">The contact.</param>
        /// <param name="nickname">The new nickname.</param>
        internal void RenameContact(Contact contact, string nickname)
        {
            jabber.protocol.iq.Item it = new jabber.protocol.iq.Item(this.m_Client.Document);
            it.Nickname = nickname;
            it.JID = new jabber.JID(contact.User, contact.Server, "");
            this.m_Roster.Modify(it);
            contact.SetData(it);
        }

        /// <summary>
        /// Removes a contact from the user's roster via the Remove function.
        /// </summary>
        /// <param name="contact"></param>
        internal void RemoveContact(Contact contact)
        {
            this.m_Roster.Remove(new jabber.JID(contact.User, contact.Server, ""));
        }

        /// <summary>
        /// Sets the current user's state.
        /// </summary>
        /// <param name="state">The new state.</param>
        internal void SetUserState(Contact.StateEnum state)
        {
            switch (state)
            {
                case Contact.StateEnum.ONLINE:
                case Contact.StateEnum.AWAY:
                case Contact.StateEnum.EXTENDEDAWAY:
                case Contact.StateEnum.BUSY:
                case Contact.StateEnum.INVISIBLE:
                    string show = null;
                    jabber.protocol.client.PresenceType t = jabber.protocol.client.PresenceType.available;
                    switch (state)
                    {
                        case Contact.StateEnum.ONLINE:
                            show = null;
                            break;
                        case Contact.StateEnum.AWAY:
                            show = "away";
                            break;
                        case Contact.StateEnum.EXTENDEDAWAY:
                            show = "xa";
                            break;
                        case Contact.StateEnum.BUSY:
                            show = "dnd";
                            break;
                        case Contact.StateEnum.INVISIBLE:
                            show = null;
                            t = jabber.protocol.client.PresenceType.invisible;
                            break;
                    }

                    if (this.p_Status.Trim() == "" || this.p_Status.StartsWith("RoketPack: "))
                    {
                        switch (state)
                        {
                            case Contact.StateEnum.ONLINE:
                                this.p_Status = "RoketPack: Online";
                                break;
                            case Contact.StateEnum.AWAY:
                                this.p_Status = "RoketPack: Away";
                                break;
                            case Contact.StateEnum.EXTENDEDAWAY:
                                this.p_Status = "RoketPack: Extended Away";
                                break;
                            case Contact.StateEnum.BUSY:
                                this.p_Status = "RoketPack: Busy";
                                break;
                            case Contact.StateEnum.INVISIBLE:
                                this.p_Status = "RoketPack: Invisible";
                                break;
                            case Contact.StateEnum.OFFLINE:
                                this.p_Status = "RoketPack: Offline";
                                break;
                        }
                    }

                    // Store our presence call in a lambda since we may
                    // need to delay it's calling until the OnAuthenticated
                    // event fires if we are not currently online.
                    RoketPack.Manager.VoidLambda lambda = () =>
                        {
                            int priority = 0;
                            if (state == Contact.StateEnum.ONLINE) priority = 100;
                            this.m_Client.Presence(
                                t,
                                this.p_Status,
                                show,
                                priority);

                            // We have to manually update the invisible status since
                            // there's notification.
                            if (t == jabber.protocol.client.PresenceType.invisible)
                                this.p_State = Contact.StateEnum.INVISIBLE;
                        };

                    // Now either call the lambda directly if we're online,
                    // or go online and call it upon authentication.
                    if (this.m_Connected)
                        lambda();
                    else
                    {
                        this.Connect();
                        this.m_Client.OnAuthenticate += new bedrock.ObjectHandler((target) =>
                            {
                                lambda();
                            });
                    }
                    break;
                case Contact.StateEnum.OFFLINE:
                    if (this.m_Connected)
                        this.Disconnect();
                    this.p_State = Contact.StateEnum.OFFLINE;
                    break;
            }
        }

        /// <summary>
        /// The user's current text status.
        /// </summary>
        public string Status
        {
            get
            {
                return this.p_Status;
            }
            set
            {
                this.p_Status = value;
            }
        }

        /// <summary>
        /// The user's current online / away / offline state.
        /// </summary>
        public Contact.StateEnum State
        {
            get
            {
                return this.p_State;
            }
            set
            {
                this.p_State = value;
            }
        }

        /// <summary>
        /// The user's contacts.
        /// </summary>
        internal List<Contact> Contacts
        {
            get
            {
                return this.p_Contacts;
            }
        }

        /// <summary>
        /// The representation of the user (preferring the user's nickname but falls
        /// back to username@domain.com if not available).
        /// </summary>
        internal string UserRepresentation
        {
            get
            {
                jabber.protocol.iq.Item item = this.m_Roster[this.m_Client.JID];
                if (item == null)
                    return this.p_Username + "@" + this.p_Server;
                else if (item.Nickname == null || item.Nickname.Trim() == "")
                    return item.JID.User + "@" + item.JID.Server;
                else
                    return item.Nickname;
            }
        }

        /// <summary>
        /// A reference to the current IM messaging queue.
        /// </summary>
        internal MessageQueue Queue
        {
            get
            {
                return this.p_Queue;
            }
        }

        /// <summary>
        /// The user's username.
        /// </summary>
        internal string Username
        {
            get
            {
                return this.p_Username;
            }
        }

        /// <summary>
        /// The user's server (domain).
        /// </summary>
        internal string Server
        {
            get
            {
                return this.p_Server;
            }
        }
    }

    /// <summary>
    /// A class which represents an IM contact.
    /// </summary>
    public class Contact
    {
        private jabber.protocol.iq.Item p_ContactData = null;
        private jabber.JID p_JID = null;
        private string p_Status = null;
        private StateEnum p_State = StateEnum.UNKNOWN;
        private FriendUI m_FriendUI = null;
        private List<ContactTreeNode> m_FriendUINodes = new List<ContactTreeNode>();

        /// <summary>
        /// An enumeration representing the different online states.
        /// </summary>
        public enum StateEnum
        {
            ONLINE,
            BUSY,
            AWAY,
            EXTENDEDAWAY,
            INVISIBLE,
            OFFLINE,
            UNKNOWN
        }

        /// <summary>
        /// Creates a new Contact instance from Jabber Item data and the raw
        /// presence data.
        /// </summary>
        /// <param name="data">The IM data associated with this contact.</param>
        /// <param name="presence">The presence data.</param>
        internal Contact(jabber.protocol.iq.Item data, jabber.protocol.client.Presence presence)
        {
            this.p_ContactData = data;
            this.p_JID = data.JID;
            this.p_State = Contact.GetStateFromPresence(presence);
            if (presence != null)
                this.p_Status = presence.Status;
        }

        /// <summary>
        /// Creates a new Contact instance from a Jabber ID and the presence
        /// manager.
        /// </summary>
        /// <param name="jid">The Jabber ID</param>
        /// <param name="manager">The presence manager.</param>
        internal Contact(jabber.JID jid, PresenceManager manager)
        {
            this.p_JID = jid;
            this.p_State = StateEnum.INVISIBLE;
            if (manager != null && manager[this.p_JID] != null)
                this.p_Status = manager[this.p_JID].Status;
        }

        /// <summary>
        /// An event which is triggered when the contact's status is updated.
        /// </summary>
        internal event ContactEventHandler StatusUpdated;
        internal virtual void OnStatusUpdated()
        {
            if (this.StatusUpdated != null)
                this.StatusUpdated(this, new ContactEventArgs(this, ContactEventArgs.ActionType.STATUS));
        }

        /// <summary>
        /// Adds the contact to a group (local only).
        /// </summary>
        /// <param name="name">The name of the group.</param>
        internal void AddGroup(string name)
        {
            if (this.p_ContactData != null)
                this.p_ContactData.AddGroup(name);
        }

        /// <summary>
        /// Sets the new jabber.protocol.iq.Item object to be associated
        /// with this contact.  Used after the contact has been renamed.
        /// </summary>
        /// <param name="data">The new contact data.</param>
        internal void SetData(jabber.protocol.iq.Item data)
        {
            this.p_ContactData = data;
        }

        /// <summary>
        /// The contact's unique ID.
        /// </summary>
        internal string ID
        {
            get
            {
                return this.p_JID.Bare;
            }
        }

        /// <summary>
        /// The username representation of this contact.  Prefers the contact's nickname, but
        /// if it is not available, falls back to username@domain.com.
        /// </summary>
        internal string Representation
        {
            get
            {
                if (this.p_ContactData == null || this.p_ContactData.Nickname == null || this.p_ContactData.Nickname.Trim() == "")
                    return this.p_JID.User + "@" + this.p_JID.Server;
                else
                    return this.p_ContactData.Nickname;
            }
        }

        /// <summary>
        /// The contact's username.
        /// </summary>
        internal string User
        {
            get
            {
                return this.p_JID.User;
            }
        }

        /// <summary>
        /// The contact's server.
        /// </summary>
        internal string Server
        {
            get
            {
                return this.p_JID.Server;
            }
        }

        /// <summary>
        /// The groups that this contact belongs to.
        /// </summary>
        internal jabber.protocol.iq.Group[] Groups
        {
            get
            {
                if (this.p_ContactData != null)
                    return this.p_ContactData.GetGroups();
                else
                    return null;
            }
        }

        /// <summary>
        /// The contact's textual status.
        /// </summary>
        internal string Status
        {
            get
            {
                return this.p_Status;
            }
            set
            {
                this.p_Status = value;
            }
        }

        /// <summary>
        /// Whether the contact is currently considered to be online.
        /// </summary>
        internal bool Online
        {
            get
            {
                return !(this.p_State == StateEnum.OFFLINE ||
                    this.p_State == StateEnum.INVISIBLE ||
                    this.p_State == StateEnum.UNKNOWN);
            }
        }

        /// <summary>
        /// The contact's current offline / away / online state.
        /// </summary>
        internal StateEnum State
        {
            get
            {
                return this.p_State;
            }
            set
            {
                this.p_State = value;
                if (this.m_FriendUI != null)
                {
                    RoketPack.Manager.VoidLambda lambda = () =>
                        {
                            foreach (ContactTreeNode node in this.m_FriendUINodes)
                            {
                                int index = this.m_FriendUI.Items.IndexOf(node);
                                if (index == -1)
                                    continue;

                                System.Drawing.Rectangle region =
                                    this.m_FriendUI.GetItemRectangle(index);
                                this.m_FriendUI.Invalidate(region);
                            }

                            List<GroupTreeNode> groupnodes = new List<GroupTreeNode>();
                            foreach (object item in this.m_FriendUI.Items)
                            {
                                if (item is GroupTreeNode)
                                    groupnodes.Add((GroupTreeNode)item);
                            }
                            foreach (GroupTreeNode node in groupnodes)
                            {
                                foreach (jabber.protocol.iq.Group group in this.Groups)
                                {
                                    if (node.Name == group.GroupName)
                                    {
                                        node.Expanded = false;
                                        node.Expanded = true;
                                    }
                                }
                            }
                        };

                    if (this.m_FriendUI.InvokeRequired)
                        this.m_FriendUI.Invoke(lambda);
                    else
                        lambda();
                }
            }
        }

        /// <summary>
        /// The image which should be used to represent this contact's
        /// online / away / offline state.
        /// </summary>
        internal System.Drawing.Image StateImage
        {
            get
            {
                switch (this.State)
                {
                    case StateEnum.ONLINE:
                        return Properties.Resources.IMOnline;
                    case StateEnum.BUSY:
                        return Properties.Resources.IMBusy;
                    case StateEnum.AWAY:
                        return Properties.Resources.IMAway;
                    case StateEnum.EXTENDEDAWAY:
                        return Properties.Resources.IMExtendedAway;
                    case StateEnum.INVISIBLE:
                        return Properties.Resources.IMInvisible;
                    case StateEnum.OFFLINE:
                        return Properties.Resources.IMOffline;
                    default:
                        return Properties.Resources.IMOffline;
                }
            }
        }

        /// <summary>
        /// Retrieves the state data from the specified presence data.
        /// </summary>
        /// <param name="p">The presence data.</param>
        /// <returns>The state of the contact defined in the presence data.</returns>
        internal static StateEnum GetStateFromPresence(jabber.protocol.client.Presence p)
        {
            if ((p == null) || (p.Type == jabber.protocol.client.PresenceType.unavailable))
                return StateEnum.OFFLINE;

            switch (p.Show)
            {
                case null:
                case "":
                    return StateEnum.ONLINE;
                case "away":
                    return StateEnum.AWAY;
                case "xa":
                    return StateEnum.EXTENDEDAWAY;
                case "dnd":
                    return StateEnum.BUSY;
                case "chat":
                    return StateEnum.ONLINE;
            }

            return StateEnum.OFFLINE;
        }

        /// <summary>
        /// Associates the current contact with a FriendUI component and the
        /// specified node within that list.
        /// </summary>
        /// <param name="ui">The FriendUI component that holds the node.</param>
        /// <param name="node">The specified node.</param>
        internal void AssociateListItem(FriendUI ui, ContactTreeNode node)
        {
            this.m_FriendUI = ui;
            if (!this.m_FriendUINodes.Contains(node))
                this.m_FriendUINodes.Add(node);
        }
    }

    /// <summary>
    /// A delegate used for contact events.
    /// </summary>
    internal delegate void ContactEventHandler(object sender, ContactEventArgs e);

    /// <summary>
    /// The class used for passing contact specified information to contact events.
    /// </summary>
    internal class ContactEventArgs
    {
        private Contact p_Contact = null;
        private ActionType p_Action = ActionType.NONE;
        private string p_Nickname = "";

        /// <summary>
        /// An enumeration representing the type of action being taken.
        /// </summary>
        internal enum ActionType
        {
            NONE,
            ADDED,
            CHANGED,
            REMOVED,
            STATUS,
            NICKNAME,
            CHAT
        }

        /// <summary>
        /// Constructs a new ContactEventArgs instance with the specified
        /// Contact performing the specified action.
        /// </summary>
        /// <param name="contact">The contact.</param>
        /// <param name="action">The action.</param>
        internal ContactEventArgs(Contact contact, ActionType action)
        {
            this.p_Contact = contact;
            this.p_Action = action;
        }

        /// <summary>
        /// Constructs a new ContactEventArgs instance with the specified
        /// Contact performing the specified action with the new nickname.
        /// </summary>
        /// <param name="contact">The contact.</param>
        /// <param name="action">The action.</param>
        /// <param name="action">The new nickname for the contact.</param>
        internal ContactEventArgs(Contact contact, ActionType action, string nickname)
        {
            this.p_Contact = contact;
            this.p_Action = action;
            this.p_Nickname = nickname;
        }
        
        /// <summary>
        /// The contact associated with this event.
        /// </summary>
        internal Contact Contact
        {
            get
            {
                return this.p_Contact;
            }
        }

        /// <summary>
        /// The action being undertaken by this event.
        /// </summary>
        internal ActionType Action
        {
            get
            {
                return this.p_Action;
            }
        }

        /// <summary>
        /// The new nickname for the contact.  Only applicable if Action is ActionType.NICKNAME.
        /// </summary>
        internal string Nickname
        {
            get
            {
                return this.p_Nickname;
            }
        }
    }
}
